iT邦幫忙

2025 iThome 鐵人賽

DAY 27
0
Rust

Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計系列 第 27

(Day27) Rust unsafe 的最小暴露面:把風險關在最小區域

  • 分享至 

  • xImage
  •  

https://ithelp.ithome.com.tw/upload/images/20250917/20124462KA2M7PfuNm.png

Rust 逼我成為更好的工程師:unsafe 的最小暴露面:把風險關在最小區域

在前面的篇章中,我們深入理解了 Rust 的安全保證。

今天我們要面對一個現實:有時候,我們必須繞過編譯器的檢查

關鍵問題是:如何在必要時使用 unsafe,同時將風險控制在最小範圍?

unsafe 不是「不安全」,是「承諾」

unsafe 的本質

// unsafe 是一個承諾:
// "我保證這段程式碼遵守 Rust 的安全規則,
//  即使編譯器無法驗證"

unsafe {
    // 編譯器信任你
    // 但責任在你身上
}

關鍵洞察unsafe 不是關閉安全檢查,而是將檢查的責任從編譯器轉移到程式設計師。

unsafe 能做什麼?

// 只有五件事需要 unsafe:

// 1. 解引用裸指標
unsafe {
    let ptr = 0x12345678 as *const i32;
    let value = *ptr;  // 可能指向無效記憶體
}

// 2. 呼叫 unsafe 函式或方法
unsafe {
    libc::malloc(1024);
}

// 3. 存取或修改可變靜態變數
static mut COUNTER: i32 = 0;
unsafe {
    COUNTER += 1;
}

// 4. 實作 unsafe trait
unsafe trait UnsafeTrait {}
unsafe impl UnsafeTrait for MyType {}

// 5. 存取 union 的欄位
union MyUnion {
    i: i32,
    f: f32,
}
let u = MyUnion { i: 42 };
unsafe {
    let f = u.f;
}

unsafe 不能做什麼?

// unsafe 不會關閉借用檢查
unsafe {
    let mut x = 5;
    let r1 = &x;
    let r2 = &mut x;  // ❌ 仍然是錯誤
}

// unsafe 不會關閉生命週期檢查
unsafe {
    let r;
    {
        let x = 5;
        r = &x;  // ❌ 仍然是錯誤
    }
}

最小化 unsafe 的範圍

問題:unsafe 洩漏到整個函式

// 糟糕:整個函式都是 unsafe
unsafe fn bad_function(ptr: *const i32, len: usize) -> Vec<i32> {
    let mut result = Vec::new();
    for i in 0..len {
        result.push(*ptr.add(i));  // 只有這行需要 unsafe
    }
    result
}

// 呼叫者也必須寫 unsafe
fn caller() {
    let data = vec![1, 2, 3];
    unsafe {
        let result = bad_function(data.as_ptr(), data.len());
    }
}

解決方案:最小化 unsafe 區域

// 好的:只有必要的部分是 unsafe
fn good_function(ptr: *const i32, len: usize) -> Vec<i32> {
    let mut result = Vec::new();
    for i in 0..len {
        unsafe {
            result.push(*ptr.add(i));  // 只有這行是 unsafe
        }
    }
    result
}

// 更好的:提供安全的包裝
fn best_function(slice: &[i32]) -> Vec<i32> {
    // 完全安全的介面
    slice.to_vec()
}

安全外殼模式

模式 1:驗證前置條件

// 不安全的內部實作
unsafe fn raw_read(ptr: *const u8, len: usize) -> Vec<u8> {
    let mut buffer = Vec::with_capacity(len);
    std::ptr::copy_nonoverlapping(ptr, buffer.as_mut_ptr(), len);
    buffer.set_len(len);
    buffer
}

// 安全的公開介面
pub fn safe_read(ptr: *const u8, len: usize) -> Result<Vec<u8>, String> {
    // 驗證前置條件
    if ptr.is_null() {
        return Err("指標不能為 null".to_string());
    }
    
    if len == 0 {
        return Ok(Vec::new());
    }
    
    // 在驗證後才呼叫 unsafe
    unsafe {
        Ok(raw_read(ptr, len))
    }
}

模式 2:RAII 包裝

use std::ptr;

// 不安全的 C 資源
extern "C" {
    fn create_resource() -> *mut Resource;
    fn destroy_resource(ptr: *mut Resource);
    fn use_resource(ptr: *mut Resource) -> i32;
}

// 安全的 RAII 包裝
pub struct SafeResource {
    ptr: *mut Resource,
}

impl SafeResource {
    pub fn new() -> Result<Self, String> {
        unsafe {
            let ptr = create_resource();
            if ptr.is_null() {
                Err("建立資源失敗".to_string())
            } else {
                Ok(SafeResource { ptr })
            }
        }
    }
    
    // 提供安全的方法
    pub fn use_it(&self) -> Result<i32, String> {
        unsafe {
            let result = use_resource(self.ptr);
            if result >= 0 {
                Ok(result)
            } else {
                Err("操作失敗".to_string())
            }
        }
    }
}

impl Drop for SafeResource {
    fn drop(&mut self) {
        unsafe {
            destroy_resource(self.ptr);
        }
    }
}

// 使用者完全不需要寫 unsafe
fn user_code() -> Result<(), String> {
    let resource = SafeResource::new()?;
    let result = resource.use_it()?;
    println!("結果: {}", result);
    Ok(())
}

模式 3:不變式文件化

/// 一個安全的環形緩衝區
/// 
/// # 不變式
/// - `read_pos` 和 `write_pos` 永遠在 `0..capacity` 範圍內
/// - 緩衝區永遠不會超過容量
/// - 所有索引計算都經過邊界檢查
pub struct RingBuffer<T> {
    buffer: Vec<T>,
    read_pos: usize,
    write_pos: usize,
    capacity: usize,
}

impl<T> RingBuffer<T> {
    pub fn new(capacity: usize) -> Self {
        assert!(capacity > 0, "容量必須大於 0");
        
        RingBuffer {
            buffer: Vec::with_capacity(capacity),
            read_pos: 0,
            write_pos: 0,
            capacity,
        }
    }
    
    pub fn push(&mut self, value: T) -> Result<(), T> {
        if self.is_full() {
            return Err(value);
        }
        
        // 安全:我們已經檢查過容量
        unsafe {
            let ptr = self.buffer.as_mut_ptr().add(self.write_pos);
            ptr.write(value);
        }
        
        // 維護不變式
        self.write_pos = (self.write_pos + 1) % self.capacity;
        Ok(())
    }
    
    fn is_full(&self) -> bool {
        (self.write_pos + 1) % self.capacity == self.read_pos
    }
}

實戰案例:實作 Vec

簡化版的 Vec 實作

use std::alloc::{alloc, dealloc, Layout};
use std::ptr;

pub struct MyVec<T> {
    ptr: *mut T,
    len: usize,
    capacity: usize,
}

impl<T> MyVec<T> {
    pub fn new() -> Self {
        MyVec {
            ptr: ptr::null_mut(),
            len: 0,
            capacity: 0,
        }
    }
    
    pub fn push(&mut self, value: T) {
        if self.len == self.capacity {
            self.grow();
        }
        
        unsafe {
            // 安全:我們確保有足夠容量
            let ptr = self.ptr.add(self.len);
            ptr.write(value);
        }
        
        self.len += 1;
    }
    
    pub fn pop(&mut self) -> Option<T> {
        if self.len == 0 {
            return None;
        }
        
        self.len -= 1;
        
        unsafe {
            // 安全:len 現在指向最後一個元素
            Some(ptr::read(self.ptr.add(self.len)))
        }
    }
    
    pub fn get(&self, index: usize) -> Option<&T> {
        if index >= self.len {
            return None;
        }
        
        unsafe {
            // 安全:我們已經檢查過邊界
            Some(&*self.ptr.add(index))
        }
    }
    
    fn grow(&mut self) {
        let new_capacity = if self.capacity == 0 {
            1
        } else {
            self.capacity * 2
        };
        
        let new_layout = Layout::array::<T>(new_capacity).unwrap();
        
        let new_ptr = if self.capacity == 0 {
            unsafe { alloc(new_layout) as *mut T }
        } else {
            let old_layout = Layout::array::<T>(self.capacity).unwrap();
            unsafe {
                let ptr = alloc(new_layout) as *mut T;
                ptr::copy_nonoverlapping(self.ptr, ptr, self.len);
                dealloc(self.ptr as *mut u8, old_layout);
                ptr
            }
        };
        
        self.ptr = new_ptr;
        self.capacity = new_capacity;
    }
}

impl<T> Drop for MyVec<T> {
    fn drop(&mut self) {
        if self.capacity == 0 {
            return;
        }
        
        // 呼叫所有元素的 Drop
        while let Some(_) = self.pop() {}
        
        // 釋放記憶體
        let layout = Layout::array::<T>(self.capacity).unwrap();
        unsafe {
            dealloc(self.ptr as *mut u8, layout);
        }
    }
}

// 使用者完全不需要寫 unsafe
fn use_my_vec() {
    let mut vec = MyVec::new();
    vec.push(1);
    vec.push(2);
    vec.push(3);
    
    assert_eq!(vec.get(1), Some(&2));
    assert_eq!(vec.pop(), Some(3));
}

unsafe trait:標記特殊保證

什麼時候需要 unsafe trait?

// Send 和 Sync 是 unsafe trait
unsafe trait Send {}
unsafe trait Sync {}

// 為什麼是 unsafe?
// 因為實作者必須保證執行緒安全
// 編譯器無法自動驗證

// 自定義 unsafe trait
unsafe trait TrustedLen: Iterator {
    // 承諾:size_hint() 回傳的上界是精確的
}

// 實作時必須寫 unsafe
unsafe impl<T> TrustedLen for std::vec::IntoIter<T> {}

實戰案例:實作 Send

use std::marker::PhantomData;

// 預設情況:包含裸指標的型別不是 Send
struct NotSend {
    ptr: *mut i32,
}

// 如果我們確定這是安全的,可以手動實作
struct IsSend {
    ptr: *mut i32,
    _marker: PhantomData<i32>,  // 標記所有權
}

unsafe impl Send for IsSend {}
// 承諾:這個型別可以安全地在執行緒間傳遞

// 使用時的責任
fn use_send() {
    let data = Box::new(42);
    let ptr = Box::into_raw(data);
    
    let wrapper = IsSend {
        ptr,
        _marker: PhantomData,
    };
    
    // 可以傳遞到其他執行緒
    std::thread::spawn(move || {
        unsafe {
            println!("{}", *wrapper.ptr);
            // 必須確保正確釋放
            let _ = Box::from_raw(wrapper.ptr);
        }
    });
}

unsafe 的審查清單

1. 是否真的需要 unsafe?

// 問自己:
// - 有沒有安全的替代方案?
// - 能不能用標準庫的安全抽象?
// - 效能提升是否值得增加的風險?

// 通常不需要 unsafe
fn safe_version(slice: &[i32]) -> i32 {
    slice.iter().sum()
}

// 只在效能關鍵路徑才考慮 unsafe
fn unsafe_version(ptr: *const i32, len: usize) -> i32 {
    let mut sum = 0;
    for i in 0..len {
        unsafe {
            sum += *ptr.add(i);
        }
    }
    sum
}

2. 前置條件是否被驗證?

// 糟糕:沒有驗證
unsafe fn bad(ptr: *const i32) -> i32 {
    *ptr  // 如果 ptr 是 null 怎麼辦?
}

// 好的:驗證前置條件
fn good(ptr: *const i32) -> Result<i32, String> {
    if ptr.is_null() {
        return Err("指標不能為 null".to_string());
    }
    
    unsafe {
        Ok(*ptr)
    }
}

3. 不變式是否被維護?

// 文件化所有不變式
/// # 不變式
/// - `len <= capacity`
/// - `ptr` 指向至少 `capacity` 個 T 的有效記憶體
/// - 前 `len` 個元素已初始化
struct Container<T> {
    ptr: *mut T,
    len: usize,
    capacity: usize,
}

impl<T> Container<T> {
    // 每個方法都必須維護不變式
    fn push(&mut self, value: T) {
        assert!(self.len <= self.capacity);  // 檢查不變式
        
        if self.len == self.capacity {
            self.grow();
        }
        
        unsafe {
            self.ptr.add(self.len).write(value);
        }
        
        self.len += 1;
        
        assert!(self.len <= self.capacity);  // 確保不變式
    }
}

4. 是否有測試覆蓋?

#[cfg(test)]
mod tests {
    use super::*;
    
    #[test]
    fn test_unsafe_function() {
        // 測試正常情況
        let data = vec![1, 2, 3];
        let result = unsafe_function(data.as_ptr(), data.len());
        assert_eq!(result, 6);
    }
    
    #[test]
    fn test_edge_cases() {
        // 測試邊界情況
        let empty: Vec<i32> = vec![];
        let result = unsafe_function(empty.as_ptr(), 0);
        assert_eq!(result, 0);
    }
    
    #[test]
    #[should_panic]
    fn test_invalid_input() {
        // 測試無效輸入
        unsafe_function(std::ptr::null(), 10);
    }
}

總結:unsafe 的使用原則

1. 最小化範圍

// 只在必要的地方使用 unsafe
fn minimal_unsafe() {
    let safe_part = prepare_data();
    
    unsafe {
        // 只有這一行是 unsafe
        dangerous_operation();
    }
    
    process_result();
}

2. 提供安全介面

// 內部 unsafe,外部安全
pub struct SafeWrapper {
    inner: UnsafeInner,
}

impl SafeWrapper {
    pub fn safe_method(&self) {
        // 內部處理 unsafe
    }
}

3. 文件化假設

/// # Safety
/// 
/// 呼叫者必須確保:
/// - `ptr` 指向至少 `len` 個有效的 T
/// - `ptr` 在呼叫期間保持有效
/// - 沒有其他執行緒同時存取這塊記憶體
pub unsafe fn documented_unsafe(ptr: *const T, len: usize) {
    // 實作
}

4. 充分測試

#[cfg(test)]
mod tests {
    // 測試所有邊界情況
    // 使用 miri 檢測未定義行為
    // 使用 sanitizers 檢測記憶體錯誤
}

關鍵洞察unsafe 不是逃生門,而是一個需要極度謹慎的工具。好的 unsafe 程式碼應該將風險完全封裝在內部,對外提供完全安全的介面。

在下一篇中,我們將探討 診斷與記憶體檢查,看看如何系統性地驗證我們的假設。

相關連結與參考資源


上一篇
(Day26) Rust 進階智慧指標:Pin、特徵物件與動態派發的界線
系列文
Rust 逼我成為更好的工程師:從 Borrow Checker 看軟體設計27
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言